package provider

import (
	"context"
	"fmt"
	"strings"

	golibvirt "github.com/digitalocean/go-libvirt"
	"github.com/dmacvicar/terraform-provider-libvirt/v2/internal/generated"
	libvirtclient "github.com/dmacvicar/terraform-provider-libvirt/v2/internal/libvirt"
	"github.com/hashicorp/terraform-plugin-framework/path"
	"github.com/hashicorp/terraform-plugin-framework/resource"
	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
	"github.com/hashicorp/terraform-plugin-framework/types"
	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
	"github.com/hashicorp/terraform-plugin-log/tflog"
	"libvirt.org/go/libvirtxml"
)

// NetworkResource uses 100% generated code
type NetworkResource struct {
	client *libvirtclient.Client
}

// NetworkResourceModel extends generated model with resource-specific fields
type NetworkResourceModel struct {
	generated.NetworkModel
	ID        types.String `tfsdk:"id"`        // Resource identifier (UUID)
	Autostart types.Bool   `tfsdk:"autostart"` // Provider-specific: whether to autostart
}

func NewNetworkResource() resource.Resource {
	return &NetworkResource{}
}

func (r *NetworkResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
	resp.TypeName = req.ProviderTypeName + "_network"
}

func (r *NetworkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	// Get base bridge schema to override name as computed
	baseBridgeAttr := mustSingleNestedAttribute(generated.NetworkBridgeSchemaAttribute(), "NetworkBridge")
	bridgeAttrs := baseBridgeAttr.Attributes
	bridgeAttrs["name"] = schema.StringAttribute{
		Description: "Bridge name (generated by libvirt if not specified)",
		Optional:    true,
		Computed:    true,
		PlanModifiers: []planmodifier.String{
			stringplanmodifier.UseStateForUnknown(),
		},
	}

	// Use generated schema with resource-specific overrides
	resp.Schema = generated.NetworkSchema(map[string]schema.Attribute{
		"id": schema.StringAttribute{
			Description: "Network identifier (UUID)",
			Computed:    true,
			PlanModifiers: []planmodifier.String{
				stringplanmodifier.UseStateForUnknown(),
			},
		},
		"bridge": schema.SingleNestedAttribute{
			Optional:   true,
			Attributes: bridgeAttrs,
		},
		"autostart": schema.BoolAttribute{
			Description: "Whether the network should be started automatically when the host boots",
			Optional:    true,
			Computed:    true,
		},
	})
}

func (r *NetworkResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
	if req.ProviderData == nil {
		return
	}

	client, ok := req.ProviderData.(*libvirtclient.Client)
	if !ok {
		resp.Diagnostics.AddError(
			"Unexpected Resource Configure Type",
			fmt.Sprintf("Expected *libvirt.Client, got: %T", req.ProviderData),
		)
		return
	}

	r.client = client
}

func (r *NetworkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	var model NetworkResourceModel
	resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
	if resp.Diagnostics.HasError() {
		return
	}

	// Convert model to libvirt XML using generated conversion
	networkXML, err := generated.NetworkToXML(ctx, &model.NetworkModel)
	if err != nil {
		resp.Diagnostics.AddError(
			"Model to XML Conversion Failed",
			fmt.Sprintf("Failed to convert model to XML: %s", err),
		)
		return
	}

	// Marshal to XML
	xmlDoc, err := networkXML.Marshal()
	if err != nil {
		resp.Diagnostics.AddError(
			"XML Marshaling Failed",
			fmt.Sprintf("Failed to marshal network XML: %s", err),
		)
		return
	}

	tflog.Debug(ctx, "Generated network XML", map[string]any{"xml": xmlDoc})

	// Define the network in libvirt
	net, err := r.client.Libvirt().NetworkDefineXML(xmlDoc)
	if err != nil {
		resp.Diagnostics.AddError(
			"Network Creation Failed",
			fmt.Sprintf("Failed to define network: %s", err),
		)
		return
	}

	// Set autostart if requested
	if !model.Autostart.IsNull() && !model.Autostart.IsUnknown() {
		autostart := int32(0)
		if model.Autostart.ValueBool() {
			autostart = 1
		}
		if err := r.client.Libvirt().NetworkSetAutostart(net, autostart); err != nil {
			resp.Diagnostics.AddWarning(
				"Autostart Configuration Failed",
				fmt.Sprintf("Network created but failed to set autostart: %s", err),
			)
		}
	}

	// Start the network
	if err := r.client.Libvirt().NetworkCreate(net); err != nil {
		// Cleanup: undefine the network
		if undefErr := r.client.Libvirt().NetworkUndefine(net); undefErr != nil {
			tflog.Warn(ctx, "Failed to undefine network during cleanup", map[string]any{
				"error": undefErr.Error(),
			})
		}
		resp.Diagnostics.AddError(
			"Network Start Failed",
			fmt.Sprintf("Network defined but failed to start: %s", err),
		)
		return
	}

	// Set ID
	uuidStr := libvirtclient.UUIDString(net.UUID)
	model.ID = types.StringValue(uuidStr)

	tflog.Info(ctx, "Created network", map[string]any{
		"uuid": uuidStr,
		"name": model.Name.ValueString(),
	})

	// Save the plan for preserving user intent
	planModel := model.NetworkModel

	// Read back the full state
	if err := r.readNetwork(ctx, &model, net, &planModel); err != nil {
		resp.Diagnostics.AddError(
			"Network Read Failed",
			fmt.Sprintf("Network created but failed to read back: %s", err),
		)
		return
	}

	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
}

func (r *NetworkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
	var model NetworkResourceModel
	resp.Diagnostics.Append(req.State.Get(ctx, &model)...)
	if resp.Diagnostics.HasError() {
		return
	}

	// Look up the network by UUID
	uuidStr := model.ID.ValueString()
	net, err := r.client.LookupNetworkByUUID(uuidStr)
	if err != nil {
		if strings.Contains(err.Error(), "Network not found") {
			resp.State.RemoveResource(ctx)
			return
		}
		resp.Diagnostics.AddError(
			"Network Not Found",
			fmt.Sprintf("Failed to find network: %s", err),
		)
		return
	}

	// Read network state (use current state as plan to preserve user intent)
	if err := r.readNetwork(ctx, &model, net, &model.NetworkModel); err != nil {
		resp.Diagnostics.AddError(
			"Network Read Failed",
			fmt.Sprintf("Failed to read network: %s", err),
		)
		return
	}

	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
}

func (r *NetworkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
	var model NetworkResourceModel
	resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
	if resp.Diagnostics.HasError() {
		return
	}

	var state NetworkResourceModel
	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
	if resp.Diagnostics.HasError() {
		return
	}

	// Look up the network
	uuidStr := state.ID.ValueString()
	net, err := r.client.LookupNetworkByUUID(uuidStr)
	if err != nil {
		resp.Diagnostics.AddError(
			"Network Lookup Failed",
			fmt.Sprintf("Failed to find network: %s", err),
		)
		return
	}

	// Handle autostart changes
	if !model.Autostart.Equal(state.Autostart) {
		autostart := int32(0)
		if !model.Autostart.IsNull() && !model.Autostart.IsUnknown() {
			if model.Autostart.ValueBool() {
				autostart = 1
			}
		}
		if err := r.client.Libvirt().NetworkSetAutostart(net, autostart); err != nil {
			resp.Diagnostics.AddError(
				"Autostart Update Failed",
				fmt.Sprintf("Failed to update autostart: %s", err),
			)
			return
		}
	}

	// Read back current state
	if err := r.readNetwork(ctx, &model, net, &model.NetworkModel); err != nil {
		resp.Diagnostics.AddError(
			"Network Read Failed",
			fmt.Sprintf("Network updated but failed to read back: %s", err),
		)
		return
	}

	resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
}

func (r *NetworkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
	var model NetworkResourceModel
	resp.Diagnostics.Append(req.State.Get(ctx, &model)...)
	if resp.Diagnostics.HasError() {
		return
	}

	uuidStr := model.ID.ValueString()

	// Look up the network
	net, err := r.client.LookupNetworkByUUID(uuidStr)
	if err != nil {
		if strings.Contains(err.Error(), "Network not found") {
			return
		}
		resp.Diagnostics.AddError(
			"Network Lookup Failed",
			fmt.Sprintf("Failed to find network for deletion: %s", err),
		)
		return
	}

	// Check if network is active
	active, err := r.client.Libvirt().NetworkIsActive(net)
	if err != nil {
		resp.Diagnostics.AddWarning(
			"Network Status Check Failed",
			fmt.Sprintf("Failed to check network status: %s", err),
		)
	}

	// Destroy (stop) the network if active
	if active == 1 {
		if err := r.client.Libvirt().NetworkDestroy(net); err != nil {
			resp.Diagnostics.AddError(
				"Network Stop Failed",
				fmt.Sprintf("Failed to stop network: %s", err),
			)
			return
		}
	}

	// Undefine (delete) the network
	if err := r.client.Libvirt().NetworkUndefine(net); err != nil {
		resp.Diagnostics.AddError(
			"Network Delete Failed",
			fmt.Sprintf("Failed to delete network: %s", err),
		)
		return
	}

	tflog.Info(ctx, "Deleted network", map[string]any{
		"uuid": uuidStr,
	})
}

func (r *NetworkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
	resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

// readNetwork reads network state from libvirt and populates the model
func (r *NetworkResource) readNetwork(ctx context.Context, model *NetworkResourceModel, net golibvirt.Network, plan *generated.NetworkModel) error {
	// Get network XML
	xmlDoc, err := r.client.Libvirt().NetworkGetXMLDesc(net, 0)
	if err != nil {
		return fmt.Errorf("failed to get network XML: %w", err)
	}

	// Parse XML
	var networkXML libvirtxml.Network
	if err := networkXML.Unmarshal(xmlDoc); err != nil {
		return fmt.Errorf("failed to parse network XML: %w", err)
	}

	// Convert XML to model using generated conversion
	networkModel, err := generated.NetworkFromXML(ctx, &networkXML, plan)
	if err != nil {
		return fmt.Errorf("failed to convert XML to model: %w", err)
	}

	// Update the embedded model
	model.NetworkModel = *networkModel

	// Populate computed fields that generated conversion might skip
	// bridge.name is Optional+Computed - only populate if user specified bridge in plan
	if networkXML.Bridge != nil && networkXML.Bridge.Name != "" {
		// Only populate if user specified bridge (preserving user intent for Optional fields)
		if plan != nil && !plan.Bridge.IsNull() {
			// Extract current bridge, update name, write back
			if !model.Bridge.IsNull() {
				var bridgeModel generated.NetworkBridgeModel
				bridgeDiags := model.Bridge.As(ctx, &bridgeModel, basetypes.ObjectAsOptions{})
				if !bridgeDiags.HasError() {
					// Update the name field
					bridgeModel.Name = types.StringValue(networkXML.Bridge.Name)

					// Write back
					bridgeObj, objDiags := types.ObjectValueFrom(ctx, generated.NetworkBridgeAttributeTypes(), bridgeModel)
					if !objDiags.HasError() {
						model.Bridge = bridgeObj
					}
				}
			} else {
				// User specified bridge but model doesn't have it yet, create one
				bridgeModel := generated.NetworkBridgeModel{
					Name: types.StringValue(networkXML.Bridge.Name),
				}
				bridgeObj, objDiags := types.ObjectValueFrom(ctx, generated.NetworkBridgeAttributeTypes(), bridgeModel)
				if !objDiags.HasError() {
					model.Bridge = bridgeObj
				}
			}
		} else if plan == nil {
			// Import/datasource - no plan, always populate
			var bridgeModel generated.NetworkBridgeModel
			if !model.Bridge.IsNull() {
				model.Bridge.As(ctx, &bridgeModel, basetypes.ObjectAsOptions{})
			}
			bridgeModel.Name = types.StringValue(networkXML.Bridge.Name)
			bridgeObj, objDiags := types.ObjectValueFrom(ctx, generated.NetworkBridgeAttributeTypes(), bridgeModel)
			if !objDiags.HasError() {
				model.Bridge = bridgeObj
			}
		}
		// Else: user didn't specify bridge, don't populate (preserving user intent)
	}

	// Read autostart (computed field, always populate)
	autostart, err := r.client.Libvirt().NetworkGetAutostart(net)
	if err != nil {
		model.Autostart = types.BoolValue(false)
	} else {
		model.Autostart = types.BoolValue(autostart == 1)
	}

	return nil
}
